/*
 * Copyright 2004-2014 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent
 * versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * http://ec.europa.eu/idabc/eupl
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the Licence for the specific language governing permissions and limitations
 * under the Licence.
*/

package com.eviware.soapui.security.scan;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.swing.JComponent;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.SecurityScanConfig;
import com.eviware.soapui.config.StrategyTypeConfig;
import com.eviware.soapui.config.TestAssertionConfig;
import com.eviware.soapui.impl.wsdl.AbstractWsdlModelItem;
import com.eviware.soapui.impl.wsdl.WsdlSubmitContext;
import com.eviware.soapui.impl.wsdl.support.assertions.AssertableConfig;
import com.eviware.soapui.impl.wsdl.support.assertions.AssertionsSupport;
import com.eviware.soapui.impl.wsdl.teststeps.TestRequest;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlMessageAssertion;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestRequestStep;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestStep;
import com.eviware.soapui.impl.wsdl.teststeps.assertions.TestAssertionRegistry.AssertableType;
import com.eviware.soapui.model.ModelItem;
import com.eviware.soapui.model.iface.Interface;
import com.eviware.soapui.model.iface.MessageExchange;
import com.eviware.soapui.model.iface.SubmitContext;
import com.eviware.soapui.model.security.SecurityScan;
import com.eviware.soapui.model.testsuite.Assertable;
import com.eviware.soapui.model.testsuite.AssertionError;
import com.eviware.soapui.model.testsuite.AssertionsListener;
import com.eviware.soapui.model.testsuite.ResponseAssertion;
import com.eviware.soapui.model.testsuite.SamplerTestStep;
import com.eviware.soapui.model.testsuite.TestAssertion;
import com.eviware.soapui.model.testsuite.TestStep;
import com.eviware.soapui.security.ExecutionStrategyHolder;
import com.eviware.soapui.security.Securable;
import com.eviware.soapui.security.SecurityTest;
import com.eviware.soapui.security.SecurityTestRunContext;
import com.eviware.soapui.security.SecurityTestRunner;
import com.eviware.soapui.security.SecurityTestRunnerImpl;
import com.eviware.soapui.security.result.SecurityScanRequestResult;
import com.eviware.soapui.security.result.SecurityScanResult;
import com.eviware.soapui.security.result.SecurityResult.ResultStatus;
import com.eviware.soapui.security.support.FailedSecurityMessageExchange;
import com.eviware.soapui.security.support.SecurityTestRunListener;

/**
 * Implementation that is common for all security scans. Support for security
 * workflow.
 *
 * @author robert
 */
public abstract class AbstractSecurityScan extends AbstractWsdlModelItem<SecurityScanConfig> implements
        ResponseAssertion, SecurityScan {
    private SecurityScanResult securityScanResult;
    private SecurityScanRequestResult securityScanRequestResult;
    private TestStep testStep;
    protected AssertionsSupport assertionsSupport;

    private AssertionStatus currentStatus;
    private ExecutionStrategyHolder executionStrategy;
    private TestStep originalTestStepClone;
    private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
    private boolean skipFurtherRunning;

    public AbstractSecurityScan(TestStep testStep, SecurityScanConfig config, ModelItem parent, String icon) {
        super(config, parent, icon);
        if (config == null) {
            config = SecurityScanConfig.Factory.newInstance();
            setConfig(config);
        }

        this.testStep = testStep;

        if (config.getExecutionStrategy() == null) {
            config.addNewExecutionStrategy();
            config.getExecutionStrategy().setStrategy(StrategyTypeConfig.ONE_BY_ONE);
            config.getExecutionStrategy().setDelay(100);
        } else if (config.getExecutionStrategy().getStrategy() == null) {
            config.getExecutionStrategy().setStrategy(StrategyTypeConfig.ONE_BY_ONE);
            config.getExecutionStrategy().setDelay(100);
        }

		/*
         * if security scan have no strategy, set its value to
		 * StrategyTypeConfig.NO_STRATEGY.
		 */
        setExecutionStrategy(new ExecutionStrategyHolder(config.getExecutionStrategy()));

        if (config.getCheckedParameters() == null) {
            config.addNewCheckedParameters();
        }

        initAssertions();

        setApplyForFailedTestStep(config.getApplyForFailedStep());
        if (!config.isSetDisabled()) {
            setDisabled(false);
        }
    }

    @Override
    public void copyConfig(SecurityScanConfig config) {
        super.setConfig(config);
        getConfig().setType(config.getType());
        getConfig().setName(config.getName());
        getConfig().setConfig(config.getConfig());
        getConfig().setTestStep(config.getTestStep());

        TestAssertionConfig[] assertions = config.getAssertionList().toArray(new TestAssertionConfig[0]);
        getConfig().setAssertionArray(assertions);
        initAssertions();

        getConfig().setExecutionStrategy(config.getExecutionStrategy());
        setExecutionStrategy(new ExecutionStrategyHolder(config.getExecutionStrategy()));
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.eviware.soapui.security.scan.SecurityScan#updateSecurityConfig(com
     * .eviware.soapui.config.SecurityScanConfig)
     */
    public void updateSecurityConfig(SecurityScanConfig config) {
        setConfig(config);

        assertionsSupport.refresh();

        if (executionStrategy != null && config.getExecutionStrategy() != null) {
            executionStrategy.updateConfig(config.getExecutionStrategy());
        }
    }

    protected void initAssertions() {
        assertionsSupport = new AssertionsSupport(this, new AssertableConfig() {
            public TestAssertionConfig addNewAssertion() {
                return getConfig().addNewAssertion();
            }

            public List<TestAssertionConfig> getAssertionList() {
                return getConfig().getAssertionList();
            }

            public void removeAssertion(int ix) {
                getConfig().removeAssertion(ix);
            }

            public TestAssertionConfig insertAssertion(TestAssertionConfig source, int ix) {
                TestAssertionConfig conf = getConfig().insertNewAssertion(ix);
                conf.set(source);
                return conf;
            }
        });
    }

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.security.scan.SecurityScan#run(com.eviware.soapui
     * .model.testsuite.TestStep,
     * com.eviware.soapui.security.SecurityTestRunContext,
     * com.eviware.soapui.security.SecurityTestRunner)
     */
    public SecurityScanResult run(TestStep testStep, SecurityTestRunContext context,
                                  SecurityTestRunner securityTestRunner) {
        securityScanResult = new SecurityScanResult(this);
        SecurityTestRunListener[] securityTestListeners = ((SecurityTest) getParent()).getSecurityTestRunListeners();

        PropertyChangeNotifier notifier = new PropertyChangeNotifier();
        boolean noMutations = true;
        while (hasNext(testStep, context)) {
            noMutations = false;
            if (((SecurityTestRunnerImpl) securityTestRunner).isCanceled()) {
                securityScanResult.setStatus(ResultStatus.CANCELED);
                clear();
                return securityScanResult;
            }
            securityScanRequestResult = new SecurityScanRequestResult(this);
            securityScanRequestResult.startTimer();
            originalTestStepClone = ((SecurityTestRunnerImpl) securityTestRunner)
                    .cloneForSecurityScan((WsdlTestStep) this.testStep);
            execute(securityTestRunner, originalTestStepClone, context);
            notifier.notifyChange();
            securityScanRequestResult.stopTimer();
            assertResponse(getSecurityScanRequestResult().getMessageExchange(), context);
            // add to summary result
            securityScanResult.addSecurityRequestResult(getSecurityScanRequestResult());
            for (int i = 0; i < securityTestListeners.length; i++) {
                if (Arrays.asList(((SecurityTest) getParent()).getSecurityTestRunListeners()).contains(
                        securityTestListeners[i])) {
                    securityTestListeners[i].afterSecurityScanRequest((SecurityTestRunnerImpl) securityTestRunner,
                            context, securityScanRequestResult);
                }
            }

            try {
                Thread.sleep(getExecutionStrategy().getDelay());
            } catch (InterruptedException e) {
                SoapUI.logError(e, "Security Scan Request Delay Interrupted!");
            }
        }

        if (noMutations) {
            securityScanResult.setStatus(ResultStatus.SKIPPED);
        }
        return securityScanResult;
    }

    protected void clear() {

    }

    /**
     * should be implemented in every particular scan it executes one request,
     * modified by securityScan if necessary and internally adds messages for
     * logging to SecurityScanRequestResult
     */
    abstract protected void execute(SecurityTestRunner runner, TestStep testStep, SecurityTestRunContext context);

    /**
     * checks if specific SecurityScan still has modifications left
     *
     * @param testStep2
     * @param context
     */
    abstract protected boolean hasNext(TestStep testStep2, SecurityTestRunContext context);

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#isConfigurable()
	 */

    public boolean isConfigurable() {
        return true;
    }

    /**
     * Overide if SecurityScan have Optional component
     */
    @Override
    public JComponent getComponent() {
        return null;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.security.scan.SecurityScan#getType()
     */
    public abstract String getType();

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.security.scan.SecurityScan#getTestStep()
     */
    public TestStep getTestStep() {
        return testStep;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.security.scan.SecurityScan#setTestStep(com.eviware
     * .soapui.model.testsuite.TestStep)
     */
    public void setTestStep(TestStep step) {
        testStep = step;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.security.scan.SecurityScan#isDisabled()
     */
    public boolean isDisabled() {
        return getConfig().getDisabled();
    }

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.security.scan.SecurityScan#setDisabled(boolean)
     */
    public void setDisabled(boolean disabled) {
        boolean oldValue = isDisabled();
        getConfig().setDisabled(disabled);
        pcs.firePropertyChange("disabled", oldValue, disabled);
    }

    public static boolean isSecurable(TestStep testStep) {
        if (testStep != null && testStep instanceof Securable) {
            return true;
        } else {
            return false;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.security.scan.SecurityScan#getExecutionStrategy()
     */
    public ExecutionStrategyHolder getExecutionStrategy() {
        return this.executionStrategy;
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.eviware.soapui.security.scan.SecurityScan#setExecutionStrategy(com
     * .eviware.soapui.security.ExecutionStrategyHolder)
     */
    public void setExecutionStrategy(ExecutionStrategyHolder executionStrategy) {
        ExecutionStrategyHolder oldValue = getExecutionStrategy();
        this.executionStrategy = executionStrategy;
        pcs.firePropertyChange("executionStrategy", oldValue, executionStrategy);
    }

    protected TestRequest getOriginalResult(SecurityTestRunnerImpl securityRunner, TestStep testStep) {
        testStep.run(securityRunner, securityRunner.getRunContext());

        return getRequest(testStep);
    }

    protected TestRequest getRequest(TestStep testStep) {
        if (testStep instanceof SamplerTestStep) {
            return ((SamplerTestStep) testStep).getTestRequest();
        }
        return null;
    }

    private class PropertyChangeNotifier {
        private ResultStatus oldStatus;

        public PropertyChangeNotifier() {
            oldStatus = getSecurityStatus();
        }

        public void notifyChange() {
            ResultStatus newStatus = getSecurityStatus();

            if (oldStatus != newStatus) {
                notifyPropertyChanged(STATUS_PROPERTY, oldStatus, newStatus);
            }

            oldStatus = newStatus;
        }
    }

    @Override
    public TestAssertion addAssertion(String label) {
        PropertyChangeNotifier notifier = new PropertyChangeNotifier();
        try {
            WsdlMessageAssertion assertion = assertionsSupport.addWsdlAssertion(label);
            if (assertion == null) {
                return null;
            }

            if (getAssertableContentAsXml() != null) {
                assertRequests(assertion);
                assertResponses(assertion);
                notifier.notifyChange();
            }

            return assertion;
        } catch (Exception e) {
            SoapUI.logError(e);
            return null;
        }
    }

    /**
     * @param assertion run all responses against this assertion
     */
    private void assertResponses(WsdlMessageAssertion assertion) {
        if (securityScanResult != null) {
            for (SecurityScanRequestResult result : securityScanResult.getSecurityRequestResultList()) {
                if (result.getMessageExchange() == null) {
                    return;
                }

                assertion.assertResponse(result.getMessageExchange(), new WsdlSubmitContext(testStep));
            }
        }
    }

    /**
     * @param assertion run all request against this assertion
     */
    private void assertRequests(WsdlMessageAssertion assertion) {
        if (securityScanResult != null) {
            for (SecurityScanRequestResult result : securityScanResult.getSecurityRequestResultList()) {
                if (result.getMessageExchange() == null) {
                    return;
                }

                assertion.assertRequest(result.getMessageExchange(), new WsdlSubmitContext(testStep));
            }
        }
    }

    @Override
    public void removeAssertion(TestAssertion assertion) {
        PropertyChangeNotifier notifier = new PropertyChangeNotifier();

        try {
            assertionsSupport.removeAssertion((WsdlMessageAssertion) assertion);

        } finally {
            ((WsdlMessageAssertion) assertion).release();
            notifier.notifyChange();
        }
    }

    @Override
    public TestAssertion moveAssertion(int ix, int offset) {
        WsdlMessageAssertion assertion = getAssertionAt(ix);
        PropertyChangeNotifier notifier = new PropertyChangeNotifier();

        try {
            return assertionsSupport.moveAssertion(ix, offset);
        } finally {
            ((WsdlMessageAssertion) assertion).release();
            notifier.notifyChange();
        }
    }

    @Override
    public WsdlMessageAssertion getAssertionAt(int c) {
        return assertionsSupport.getAssertionAt(c);
    }

    @Override
    public void addAssertionsListener(AssertionsListener listener) {
        assertionsSupport.addAssertionsListener(listener);
    }

    @Override
    public void removeAssertionsListener(AssertionsListener listener) {
        assertionsSupport.removeAssertionsListener(listener);
    }

    @Override
    public int getAssertionCount() {
        return assertionsSupport.getAssertionCount();
    }

    @Override
    public AssertionStatus getAssertionStatus() {
        int cnt = getAssertionCount();
        if (cnt == 0) {
            return currentStatus;
        }

        if (securityScanResult != null && securityScanResult.getStatus() == ResultStatus.OK) {
            currentStatus = AssertionStatus.VALID;
        } else {
            currentStatus = AssertionStatus.FAILED;
        }

        return currentStatus;
    }

    public ResultStatus getSecurityStatus() {
        return securityScanResult != null ? securityScanResult.getStatus() : ResultStatus.UNKNOWN;
    }

    @Override
    public String getAssertableContentAsXml() {
        if (testStep instanceof Assertable) {
            return ((Assertable) testStep).getAssertableContentAsXml();
        }

        return null;
    }

    @Override
    public String getAssertableContent() {
        if (testStep instanceof Assertable) {
            return ((Assertable) testStep).getAssertableContent();
        }

        return null;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.model.testsuite.Assertable#getAssertableType()
     *
     * Decided to go with assertions on request and response so we can implement
     * "men in the middle" attacks using monitor.
     */
    @Override
    public AssertableType getAssertableType() {
        return AssertableType.BOTH;
    }

    @Override
    public TestAssertion getAssertionByName(String name) {
        return assertionsSupport.getAssertionByName(name);
    }

    @Override
    public List<TestAssertion> getAssertionList() {
        return new ArrayList<TestAssertion>(assertionsSupport.getAssertionList());
    }

    @Override
    public Map<String, TestAssertion> getAssertions() {
        return assertionsSupport.getAssertions();
    }

    /*
     * (non-Javadoc)
     *
     * @see com.eviware.soapui.security.scan.SecurityScan#getAssertionsSupport()
     */
    public AssertionsSupport getAssertionsSupport() {
        return assertionsSupport;
    }

    @Override
    public TestAssertion cloneAssertion(TestAssertion source, String name) {
        return assertionsSupport.cloneAssertion(source, name);
    }

    @Override
    public String getDefaultAssertableContent() {
        if (testStep instanceof Assertable) {
            return ((Assertable) testStep).getDefaultAssertableContent();
        }

        return null;
    }

    @Override
    public Interface getInterface() {
        if (testStep instanceof WsdlTestRequestStep) {
            return ((WsdlTestRequestStep) testStep).getInterface();
        }

        return null;
    }

    @Override
    public ModelItem getModelItem() {
        return this;
    }

    public AssertionStatus assertResponse(MessageExchange messageExchange, SubmitContext context) {
        AssertionStatus finalResult = null;

        try {
            PropertyChangeNotifier notifier = new PropertyChangeNotifier();

            if (messageExchange != null) {
                context.setProperty(SECURITY_SCAN_REQUEST_RESULT, getSecurityScanRequestResult());

                for (WsdlMessageAssertion assertion : assertionsSupport.getAssertionList()) {
                    AssertionStatus currentResult = assertion.assertResponse(messageExchange, context);
                    updateMessages(currentResult, assertion);

                    if (finalResult == null || finalResult != AssertionStatus.FAILED) {
                        finalResult = currentResult;
                    }
                }

                setStatus(finalResult);

                notifier.notifyChange();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return finalResult;
    }

    /**
     * Sets SecurityScanStatus based on the status of all assertions added
     *
     * @param result
     */
    private void setStatus(AssertionStatus result) {
        if (result == AssertionStatus.FAILED) {
            getSecurityScanRequestResult().setStatus(ResultStatus.FAILED);
        } else if (result == AssertionStatus.VALID) {
            getSecurityScanRequestResult().setStatus(ResultStatus.OK);

        } else if (result == AssertionStatus.UNKNOWN) {
            getSecurityScanRequestResult().setStatus(ResultStatus.UNKNOWN);
        }
    }

    private void updateMessages(AssertionStatus result, WsdlMessageAssertion assertion) {
        if (result == AssertionStatus.FAILED) {
            for (AssertionError error : assertion.getErrors()) {
                getSecurityScanRequestResult().addMessage(error.getMessage());
            }
        }
    }

    // name used in configuration panel
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getConfigName()
	 */
    public abstract String getConfigName();

    // description usd in configuration panel
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getConfigDescription()
	 */
    public abstract String getConfigDescription();

    // help url used for configuration panel ( help for this scan )
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eviware.soapui.security.scan.SecurityScan#getHelpURL()
	 */
    public abstract String getHelpURL();

    protected void setSecurityScanRequestResult(SecurityScanRequestResult securityScanRequestResult) {
        this.securityScanRequestResult = securityScanRequestResult;
    }

    protected SecurityScanRequestResult getSecurityScanRequestResult() {
        return securityScanRequestResult;
    }

    /**
     * Overide if SecurityScan needs advanced settings
     */
    @Override
    public JComponent getAdvancedSettingsPanel() {
        return null;
    }

    @Override
    public SecurityScanResult getSecurityScanResult() {
        return securityScanResult;
    }

    /**
     * @param message
     */
    protected void reportSecurityScanException(String message) {
        getSecurityScanRequestResult().setMessageExchange(new FailedSecurityMessageExchange());
        getSecurityScanRequestResult().setStatus(ResultStatus.FAILED);
        getSecurityScanRequestResult().addMessage(message);
    }

    @Override
    public void addWsdlAssertion(String assertionLabel) {
        assertionsSupport.addWsdlAssertion(assertionLabel);
    }

    @Override
    public boolean isApplyForFailedStep() {
        return getConfig().getApplyForFailedStep();
    }

    @Override
    public void setApplyForFailedTestStep(boolean apply) {
        getConfig().setApplyForFailedStep(apply);
    }

    @Override
    public boolean isRunOnlyOnce() {
        return getConfig().getRunOnlyOnce();
    }

    @Override
    public void setRunOnlyOnce(boolean runOnlyOnce) {
        getConfig().setRunOnlyOnce(runOnlyOnce);
    }

    public void release() {
        if (assertionsSupport != null) {
            assertionsSupport.release();
        }

        if (securityScanResult != null) {
            securityScanResult.release();
        }

        if (securityScanRequestResult != null) {
            securityScanRequestResult.release();
        }

    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        pcs.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        pcs.removePropertyChangeListener(listener);
    }

    @Override
    public boolean isSkipFurtherRunning() {
        return skipFurtherRunning;
    }

    @Override
    public void setSkipFurtherRunning(boolean skipFurtherRunning) {
        this.skipFurtherRunning = skipFurtherRunning;
    }

}
